﻿/** \file
* \$Rev: 187 $
* 
* \$Date: 2010-04-27 21:58:51 +0000 (Tue, 27 Apr 2010) $
*
* \$URL: http://chorus-twitter.googlecode.com/svn/trunk/src/net/ParseState.cs $
*/

using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Text;
using System.Threading;
using System.Web.Script.Serialization;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
using chorus.utils;

namespace chorus.net
{
	/// <summary>
	/// The abstract parent of all parse state classes. This supports
	/// network transactions and serialization/deserialization.
	/// </summary>
	public abstract class AbstractParseState
	{
		/// <summary>The issued request.</summary>
		protected HttpWebRequest request;
		/// <summary>The received response.</summary>
		protected HttpWebResponse response;
		/// <summary>The number of active transactions.</summary>
		static int active = 0;
		/// <summary>The URI to fetch.</summary>
		protected Uri uri;
		/// <summary>The account to use to authenticate.</summary>
		protected twitter.Account account;
		/// <summary>The URI to fetch.</summary>
		public Uri Uri { get { return uri; } }
		/// <summary>The account to use to authenticate.</summary>
		public twitter.Account Account { get { return account; } }
		/// <summary>The extension for the dump file.</summary>
		public abstract string Extension { get; }

		/// <summary>Create the web request object.</summary>
		protected void CreateRequest()
		{
			request = (HttpWebRequest)HttpWebRequest.Create(uri);
			request.UserAgent = "Chorus Twitter Client: http://code.google.com/p/chorus-twitter";
			request.Timeout = 30000;
			if (account != null)
			{
				NetworkCredential creds = account.Creds;
				request.AuthenticationLevel = AuthenticationLevel.MutualAuthRequested;
				request.Credentials = creds;
				request.PreAuthenticate = true;
			}
		}

		/// <summary>Change the wait state so that a waiting cursor appears.</summary>
		/// <param name="increment">True if a wait has started; false if it has stopped.</param>
		protected void ChangeWaitState(bool increment)
		{
			if (increment)
			{
				if (++active >= 1)
				{
					Cursor.Current = Cursors.WaitCursor;
					Application.UseWaitCursor = true;
				}
			}
			else if (--active <= 0)
			{
				Cursor.Current = Cursors.Default;
				Application.UseWaitCursor = false;
			}
		}

	}

	/// <summary>
	/// The network transfer and serialization workhorse.
	/// </summary>
	/// <typeparam name="RootType">The type of serializable object.</typeparam>
	public abstract class ParseState<RootType> : AbstractParseState
	{
		/// <summary>The callback to use upon asynchronous deserialization.</summary>
		Action<RootType> deserialize;
		/// <summary>The constructor. If deserialize is non-null, initiate an asynchronous fetch.</summary>
		/// <param name="uribuilder">The URI to fetch.</param>
		/// <param name="account">The account to use to authenticate.</param>
		/// <param name="deserialize">The callback to use upon asynchronous deserialization.</param>
		public ParseState(UriBuilder uribuilder, twitter.Account account, Action<RootType> deserialize)
		{
			uribuilder.Path += "." + Extension;
			this.uri = uribuilder.Uri;
			this.account = account;

			if (deserialize != null)
			{
				this.deserialize = deserialize;
				CreateRequest();
				Thread athread = new Thread(delegate()
				{
					ChangeWaitState(true);
					request.BeginGetResponse(
						new AsyncCallback(HttpAsyncResponse), null);
				});
				athread.Name = "ParseState asynchronous start";
				athread.Start();
			}
		}

		/// <summary>Fetch and deserialize synchronously (blocking).</summary>
		/// <returns>The deserialized object.</returns>
		public RootType HttpSyncResponse()
		{
			ChangeWaitState(true);
			CreateRequest();
			RootType root;
			try
			{
				using (response = (HttpWebResponse)request.GetResponse())
					root = Deserialize();
			}
			catch (Exception e)
			{
				Program.HandleException(e);
				root = default(RootType);
			}
			finally { ChangeWaitState(false); }
			return root;
		}

		/// <summary>
		/// Fetch and deserialize synchronously (blocking). Keep trying until
		/// the request doesn't time out.
		/// </summary>
		/// <returns>The deserialized object.</returns>
		public RootType HttpSyncResponsePersistent()
		{
			ChangeWaitState(true);
			RootType root;
			while (true)
			{
				CreateRequest();
				try
				{
					using (response = (HttpWebResponse)request.GetResponse())
						root = Deserialize();
					break;
				}
				catch (Exception e)
				{
					WebException we = e as WebException;
					if (we != null && we.Status == WebExceptionStatus.Timeout)
						continue;
					Program.HandleException(e);
					root = default(RootType);
					break;
				}
			}
			ChangeWaitState(false);
			return root;
		}

		/// <summary>Called internally on an asynchronous network response.</summary>
		/// <param name="result">The asynchronous result object.</param>
		void HttpAsyncResponse(IAsyncResult result)
		{
			RootType root = default(RootType);
			try
			{
				response = (HttpWebResponse)request.EndGetResponse(result);
				root = Deserialize();
			}
			catch (Exception e) { Program.HandleException(e); }
			finally
			{
				ChangeWaitState(false);
				deserialize(root);
			}
		}

		/// <summary>Deserialize the response.</summary>
		/// <returns>The deserialized object.</returns>
		RootType Deserialize()
		{
			string content;

			using (Stream stream = response.GetResponseStream())
			using (StreamReader sreader = new StreamReader(stream))
				content = sreader.ReadToEnd();

			try { return Deserialize(content); }
			catch
			{
				using (StreamWriter dump = new StreamWriter("debug." + Extension))
					dump.Write(content);
				throw;
			}
		}

		/// <summary>Deserialize the given content string into a RootType object.</summary>
		/// <param name="content">The content to deserialize.</param>
		/// <returns>The newly deserialized object.</returns>
		protected abstract RootType Deserialize(string content);
	}

	/// <summary>
	/// A parse state to serialize and deserialize XML transactions.
	/// </summary>
	/// <typeparam name="RootType">The serializable object type.</typeparam>
	public class XmlParseState<RootType> :
		ParseState<RootType>
	{
		/// <summary>The deserialization object for regular operation.</summary>
		static XmlSerializer serial = new XmlSerializer(typeof(RootType));
		/// <summary>The deserialization object for Twitter errors.</summary>
		static XmlSerializer eserial = new XmlSerializer(typeof(ErrorResponse));
		/// <summary>The extension for the dump file.</summary>
		public override string Extension { get { return "xml"; } }

		/// <summary>The constructor.</summary>
		/// <param name="uribuilder">The URI to fetch.</param>
		/// <param name="account">The account to use to authenticate.</param>
		/// <param name="deserialize">The callback to use upon asynchronous deserialization.</param>
		public XmlParseState(UriBuilder uribuilder, twitter.Account account, Action<RootType> deserialize) :
			base(uribuilder, account, deserialize) { }

		/// <summary>Deserialize the given content string into a RootType object.</summary>
		/// <param name="content">The content to deserialize.</param>
		/// <returns>The newly deserialized object.</returns>
		protected override RootType Deserialize(string content)
		{
			RootType root = default(RootType);

			using (MemoryStream mems =
				new MemoryStream(UTF8Encoding.UTF8.GetBytes(content)))
			using (StatefulXmlReader xreader = new StatefulXmlReader(mems))
			{
				xreader.ParseState = this;
				xreader.ProhibitDtd = false;
				try
				{
					root = (RootType)serial.Deserialize(xreader);
				}
				catch (Exception e)
				{
					if (xreader.Name == "hash")
					{
						ErrorResponse err = (ErrorResponse)eserial.Deserialize(xreader);
						throw new Exception(err.Message, e);
					}
					throw;
				}
			}

			return root;
		}
	}

	/// <summary>
	/// A parse state to serialize and deserialize JSON transactions.
	/// </summary>
	/// <typeparam name="RootType">The type of serializable object.</typeparam>
	public class JsonParseState<RootType> :
		ParseState<RootType>
	{
		/// <summary>The JSON serialization object.</summary>
		StatefulJSSerializer serial = new StatefulJSSerializer();
		/// <summary>The extension for the dump file.</summary>
		public override string Extension { get { return "json"; } }

		/// <summary>The constructor.</summary>
		/// <param name="uribuilder">The URI to fetch.</param>
		/// <param name="account">The account to use to authenticate.</param>
		/// <param name="deserialize">The callback to use upon asynchronous deserialization.</param>
		public JsonParseState(UriBuilder uribuilder, twitter.Account account, Action<RootType> deserialize) :
			base(uribuilder, account, deserialize)
		{
			serial.ParseState = this;
			serial.RegisterConverters(new JavaScriptConverter[] { new TwitterJSConverter() });
		}

		/// <summary>Deserialize the given content string into a RootType object.</summary>
		/// <param name="content">The content to deserialize.</param>
		/// <returns>The newly deserialized object.</returns>
		protected override RootType Deserialize(string content)
		{
			return serial.Deserialize<RootType>(content);
		}
	}
}
